{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "973dcf47",
   "metadata": {},
   "source": [
    "## Marxan utils\n",
    "Developed between January and April 2021  \n",
    "Includes functions to run marxan and microservices locally "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "id": "c2901b2a",
   "metadata": {},
   "outputs": [],
   "source": [
    "import subprocess\n",
    "import os\n",
    "import io\n",
    "import re\n",
    "import csv\n",
    "from shutil import rmtree\n",
    "\n",
    "from datetime import datetime\n",
    "import abc\n",
    "import collections\n",
    "from typing import Generic, TypeVar, Generic, Optional, List, Type, Dict\n",
    "from pydantic import BaseModel, ValidationError, validator, Field\n",
    "from pydantic.generics import GenericModel\n",
    "from pydantic import create_model\n",
    "\n",
    "\n",
    "import pandas as pd\n",
    "import geopandas as gpd\n",
    "from IPython.display import display, JSON\n",
    "import ipywidgets as widgets\n",
    "\n",
    "import json\n",
    "\n",
    "from uuid import UUID, uuid4\n",
    "from scipy.cluster.hierarchy import linkage, fcluster\n",
    "import pandas as pd\n",
    "\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import math\n",
    "import numpy as np\n",
    "from shutil import copyfile,ignore_patterns,copytree, rmtree\n",
    "import kneed\n",
    "from scipy.interpolate import splrep, splev\n",
    "\n",
    "import random"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f95d76e4",
   "metadata": {},
   "source": [
    "## Helper clases and functions for reading and data mutation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "d0b367a0",
   "metadata": {},
   "outputs": [],
   "source": [
    "class DatFile(object):\n",
    "    \"\"\"\n",
    "    Read and write dat files.\n",
    "    \"\"\"\n",
    "    def __init__(self, file_path: str = None):\n",
    "        # Ensure the file has the right extension\n",
    "        if file_path and not file_path.endswith('.dat'):\n",
    "            raise NameError(\"File must be a '.dat' extension\")\n",
    "        \n",
    "        self.__path = file_path\n",
    "        self.data = None\n",
    "        \n",
    "    def __test_path(self, path):\n",
    "        if not self.__path and path:\n",
    "            self.__path = path\n",
    "        elif not self.__path and not path:\n",
    "            raise NameError('No path to file provided')\n",
    "    \n",
    "    def update(self, data, incremental: bool = False ):\n",
    "        \"\"\"Updates the data.\n",
    "    \n",
    "        Args:\n",
    "            data (any): The data the object should store.\n",
    "        Returns:\n",
    "            The contents of the file as a unicode string.\n",
    "        \"\"\"\n",
    "        __conversion: dict = {\n",
    "                                int: int,\n",
    "                                str: str,\n",
    "                                dict: dict,\n",
    "                                list: list\n",
    "                            }\n",
    "        if not incremental or not self.data:\n",
    "            self.data = data\n",
    "        elif type(self.data) == dict:\n",
    "            self.data.append(data)\n",
    "        else:\n",
    "            self.data = self.data + __conversion[type(self.data)](data)\n",
    "        \n",
    "    \n",
    "    def read(self, file_path: str = None):\n",
    "        \"\"\"Gets a files contents as a unicode string.\n",
    "    \n",
    "        Args:\n",
    "            filename (string): The full path to the file that will be read.\n",
    "        Returns:\n",
    "            The contents of the file as a unicode string.\n",
    "        \"\"\"\n",
    "        self.__test_path(file_path)\n",
    "        try:\n",
    "            with io.open(self.__path, mode=\"r\", encoding=\"utf-8\") as f:\n",
    "                self.data = f.readlines()\n",
    "        except (UnicodeDecodeError) as e:\n",
    "            with io.open(self.__path, mode=\"r\", encoding=\"ISO-8859-1\") as f:\n",
    "                self.data = f.read()\n",
    "        except Exception as e:\n",
    "            raise e\n",
    "    \n",
    "    def write(self, file_path: str = None):\n",
    "        \"\"\"Writes a files contents as a unicode string\n",
    "    \n",
    "        Args:\n",
    "            filename (string): The full path to the file that will be written.  \n",
    "            s (string): The unicode string to write.  \n",
    "            mode (string): Optional. The file write mode. Default value is w.  \n",
    "        Returns:\n",
    "            None  \n",
    "        \"\"\"\n",
    "        self.__test_path(file_path)\n",
    "        try:\n",
    "            with io.open(self.__path, mode, encoding=\"utf-8\") as f: \n",
    "                f.write(self.data)\n",
    "        except Exception as e:\n",
    "            raise e"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "cf9ba715",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Just in case we need to format the float numbers in a very specific way\n",
    "class MyNumber:\n",
    "    \"\"\"\n",
    "    Number formater wraper to allow a specific format string for a float number.\n",
    "    \"\"\"\n",
    "    def __init__(self, val):\n",
    "        self.val = val\n",
    "\n",
    "    def __format__(self,format_spec):\n",
    "        ss = ('{0:'+format_spec+'}').format(self.val)\n",
    "        if ( 'E' in ss):\n",
    "            mantissa, exp = ss.split('E')            \n",
    "            return mantissa + 'E'+ exp[0] + '00' + exp[1:]\n",
    "        return ss\n",
    "\n",
    "def num(s):\n",
    "    \"\"\"\n",
    "    Coerce Number transformation into float.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        return int(s)\n",
    "    except ValueError:\n",
    "        try:\n",
    "            return float(s)\n",
    "        except ValueError:\n",
    "            return s\n",
    "\n",
    "def getSizeOfNestedList(listOfElem):\n",
    "    ''' Get number of elements in a nested list'''\n",
    "    count = 0\n",
    "    # Iterate over the list\n",
    "    for elem in listOfElem:\n",
    "        # Check if type of element is list\n",
    "        if type(elem) == list:  \n",
    "            # Again call this function to get the size of this element\n",
    "            count += getSizeOfNestedList(elem)\n",
    "        else:\n",
    "            count += 1    \n",
    "    return count\n",
    "\n",
    "def _readTabularFile(filename: str)-> dict:\n",
    "    \"\"\"\n",
    "    Gets a input.dat file and outputs a dict of parameters.\n",
    "\n",
    "    Args:\n",
    "        filename (string): The full path to the file that will be read.\n",
    "    Returns:\n",
    "        The contents of the file as a dict.\n",
    "    \"\"\"\n",
    "    if filename.endswith('.dat'):\n",
    "        inputData = DatFile(filename)\n",
    "        inputData.read()\n",
    "        outputData =[]\n",
    "    \n",
    "        dialect= csv.Sniffer().sniff(inputData.data[0],[',', '\\t',' '])\n",
    "        for line in inputData.data: \n",
    "            if dialect.delimiter ==',':\n",
    "                pair= re.compile('\\s').split(line.replace('\"','').replace(' ', '_').strip('\"\"').strip('\\r').strip('\\n').replace(',', '\\t'))\n",
    "            else:\n",
    "                pair= re.compile('\\s').split(line.replace('\"','').strip('\"\"').strip('\\r').strip('\\n'))\n",
    "            outputData.append(pair)\n",
    "    \n",
    "    else:\n",
    "        df = pd.read_csv(filename)\n",
    "        df.columns = df.columns.str.rstrip().str.replace(' ','_')\n",
    "        df = df.applymap(str)\n",
    "        outputData = [df.columns.values.tolist()] + df.values.tolist()\n",
    "    \n",
    "    return outputData\n",
    "\n",
    "\n",
    "def CreateListModelFromFile(filename: str, model: Type['Model'])-> List['Model']:\n",
    "    \"\"\"\n",
    "    Gets a input.dat file and outputs a list of selected model.\n",
    "\n",
    "    Args:\n",
    "        filename (string): The full path to the file that will be read.\n",
    "        model (Type['Model']): Data model used to read and validate the data\n",
    "    Returns:\n",
    "        The contents of the file as a Data model list.\n",
    "    \"\"\"   \n",
    "    \n",
    "    if model != OutputSolutionsMatrix:\n",
    "        inputData = _readTabularFile(filename)\n",
    "    else:\n",
    "        wideData = _readTabularFile(filename)\n",
    "        inputData=[['SolutionsMatrix','PU']]\n",
    "        for x in range(1,len(wideData)):\n",
    "            pu = dict(zip(wideData[0], wideData[x]))\n",
    "            sol = pu['SolutionsMatrix']\n",
    "            del pu['SolutionsMatrix']\n",
    "            inputData.append([sol,pu])\n",
    "        \n",
    "    return [model.parse_obj(dict(zip(inputData[0], x))) for x in inputData[1:]] \n",
    "\n",
    "def CreateFileFromDF(filename: str, df: Type['Dataframe'], model: Type['Model'])-> List[list]:\n",
    "    \"\"\"\n",
    "    Gets a dataframe and outs a dat.file.\n",
    "\n",
    "    Args:\n",
    "        filename (string): The full path to the file that will be read.\n",
    "        df (Type['Dataframe']): Dataframe to save\n",
    "        model (Type['Model']): Data model used to read and validate the data\n",
    "    Returns:\n",
    "        The contents of the file as a Data model list.\n",
    "    \"\"\"\n",
    "    if model == inputDatFile:\n",
    "        data = df.transpose().to_dict('records')\n",
    "        validatedData = [inputDatFile(**x) for x in data]\n",
    "        keys = data[0].keys()\n",
    "        csv.register_dialect('dat', delimiter=' ')\n",
    "        with open(filename, 'w', encoding='utf8', newline='')  as output_file:\n",
    "            dict_writer = csv.writer(output_file, dialect='dat')\n",
    "            for row in data[0].items():\n",
    "                dict_writer.writerow(row)\n",
    "    else:\n",
    "        data = df.to_dict('records')\n",
    "        validatedData = [model(**x) for x in data]\n",
    "#         keys = validatedData[0].__dict__.keys()\n",
    "#         csv.register_dialect('dat', delimiter='\\t')\n",
    "#         with open(filename, 'w', encoding='utf8', newline='')  as output_file:\n",
    "#             dict_writer = csv.DictWriter(output_file, keys, dialect='dat')\n",
    "#             dict_writer.writeheader()\n",
    "#         dict_writer.writerows(toCSV)\n",
    "#             dict_writer.writerows(data)\n",
    "\n",
    "        if model == OutputSolutionsMatrix:\n",
    "            data = df.drop(columns=df.columns[1]).join(df[df.columns[1]].apply(pd.Series)).to_dict('records')\n",
    "            \n",
    "        keys={k: v for k, v in data[0].items() if v is not None}.keys()\n",
    "        dataNotNone = list(({key : val for key, val in sub.items() if val!= None} for sub in data)) \n",
    "\n",
    "        outputs_as_csv =[OutputRun,OutputMV,OutputSsoln,OutputSolutionsMatrix,OutputSum]\n",
    "        if model not in outputs_as_csv:\n",
    "            csv.register_dialect('dat', delimiter='\\t')\n",
    "            with open(filename, 'w', encoding='utf8', newline='')  as output_file:\n",
    "                dict_writer = csv.DictWriter(output_file, keys, dialect='dat')\n",
    "                dict_writer.writeheader()\n",
    "                dict_writer.writerows(dataNotNone)\n",
    "        else:\n",
    "            with open(filename, 'w', encoding='utf8', newline='')  as output_file:\n",
    "                dict_writer = csv.DictWriter(output_file, keys)\n",
    "                dict_writer.writeheader()\n",
    "                dict_writer.writerows(dataNotNone)\n",
    "    \n",
    "    \n",
    "    return validatedData[0:2]\n",
    "\n",
    "#def save_button(filename: str, model: Type['Model'], data: Type['QgridWidget'])-> None:\n",
    "#    \"\"\"\n",
    "#    creates a widget button and attach a on click event.\n",
    "#    \n",
    "#    Args:\n",
    "#        filename (string): The full path to the file that will be read.\n",
    "#        data (Type['QgridWidget']): Qgrid widget\n",
    "#        model (Type['Model']): Data model used to read and validate the data\n",
    "#    \"\"\"\n",
    "#    button = widgets.Button(description=\"Save\")\n",
    "#    output = widgets.Output()\n",
    "#\n",
    "#    display(button, output)\n",
    "#\n",
    "#    def on_button_clicked(b):\n",
    "#        with output:\n",
    "#            CreateFileFromDF(filename, data.get_changed_df(), model)\n",
    "#\n",
    "#    button.on_click(on_button_clicked)\n",
    "    \n",
    "def create_marxanProject(path: str)-> None:\n",
    "    os.mkdir(f'{path}/input')\n",
    "    os.mkdir(f'{path}/output')\n",
    "    copyfile('src', f'{path}')\n",
    "\n",
    "def execute_marxan(path: str)-> None:\n",
    "#     MARXAN_EXECUTABLE = f'{path}/MarOpt_v243_Linux64'\n",
    "    MARXAN_EXECUTABLE = f'{path}/marxan' # new algorithm\n",
    "    #EXECUTES MARXAN\n",
    "    # Needs to execute marxan from the marxan root folder in order to make the file find the required data.\n",
    "    os.chdir(path)\n",
    "    with subprocess.Popen([MARXAN_EXECUTABLE],\n",
    "                             stdout=subprocess.PIPE,\n",
    "                             stderr=subprocess.STDOUT,\n",
    "                             universal_newlines=True,\n",
    "                          bufsize=-1) as process:\n",
    "        while process.poll() is None:\n",
    "            output = process.stdout.readline()\n",
    "            if output:\n",
    "                print(output.strip())\n",
    "\n",
    "    os.chdir('/home/jovyan/work/notebooks/Lab')\n",
    "    \n",
    "\n",
    "def createDynamicModel(filename: str, name: str, dict_def: dict):\n",
    "    filename = f'{MARXAN_FOLDER}/{userInputFile.OUTPUTDIR}/{userInputFile.SCENNAME}_solutionsmatrix.csv'\n",
    "    file_as_list =_readTabularFile(filename)\n",
    "    file_transposed =[list(x) for x in zip(*file_as_list)] # get the colname as the key instead of the rowname\n",
    "    file_dict = {item[0]: item[1:] for item in file_t} # convert to dictionary\n",
    "    d = dict.fromkeys(file_dict.keys(), (int,...)) # extract keys to use as column inputs\n",
    "    d[list(file_dict.keys())[0]] = (str,list(file_dict.keys())[0]) \n",
    "    \n",
    "\n",
    "    fields = {}\n",
    "    for field_name,value in dict_def.items():\n",
    "        if isinstance(value,tuple):\n",
    "            fields[field_name]=value\n",
    "        elif isinstance(value,dict):\n",
    "            fields[field_name]=(dict_model(f'{name}_{field_name}',value),...)\n",
    "        else:\n",
    "            raise ValueError(f\"Field {field_name}:{value} has invalid syntax\")\n",
    "    return create_model(name,**fields)\n",
    "\n",
    "\n",
    "def readInput(MARXAN_FOLDER, MARXAN_INPUTDATA):\n",
    "    InputFile = DatFile(f'{MARXAN_FOLDER}/{MARXAN_INPUTDATA}')\n",
    "    InputFile.read()\n",
    "    userInputFile = inputDatFile.from_dat(InputFile.data)\n",
    "    \n",
    "    userInputFile.VERBOSITY = 0\n",
    "    userInputFile.SAVERUN = 3\n",
    "    userInputFile.SAVEBEST = 3\n",
    "    userInputFile.SAVESUMMARY = 3\n",
    "    userInputFile.SAVESCEN = 3\n",
    "    userInputFile.SAVETARGMET = 3\n",
    "    userInputFile.SAVESUMSOLN = 3\n",
    "    \n",
    "    userInputFile_df = pd.DataFrame.from_dict(userInputFile.__dict__, orient='index')\n",
    "    userInputFile_df= userInputFile_df.drop('BLOCKDEFNAME')\n",
    "    CreateFileFromDF(f'{MARXAN_FOLDER}/{MARXAN_INPUTDATA}',userInputFile_df,inputDatFile)   \n",
    "    return userInputFile\n",
    "\n",
    "def saveInput(MARXAN_FOLDER, MARXAN_INPUTDATA,userInputFile):\n",
    "    userInputFile_df = pd.DataFrame.from_dict(userInputFile.__dict__, orient='index')\n",
    "    userInputFile_df= userInputFile_df.drop('BLOCKDEFNAME')\n",
    "    CreateFileFromDF(f'{MARXAN_FOLDER}/{MARXAN_INPUTDATA}',userInputFile_df,inputDatFile)  \n",
    "    \n",
    "\n",
    "def validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, model):\n",
    "    userInputFile = readInput(MARXAN_FOLDER,MARXAN_INPUTDATA)\n",
    "    model_dict = {planningUnits:f'{MARXAN_FOLDER}/{userInputFile.INPUTDIR}/{userInputFile.PUNAME}',\n",
    "                  conservationFeature: f'{MARXAN_FOLDER}/{userInputFile.INPUTDIR}/{userInputFile.SPECNAME}',\n",
    "                  planningUnitVSConservationFeatureV:f'{MARXAN_FOLDER}/{userInputFile.INPUTDIR}/{userInputFile.PUVSPRNAME}',\n",
    "                  boundaryLength:f'{MARXAN_FOLDER}/{userInputFile.INPUTDIR}/{userInputFile.BOUNDNAME}',\n",
    "                  OutputRun:f'{MARXAN_FOLDER}/{userInputFile.OUTPUTDIR}/{userInputFile.SCENNAME}_best.csv', \n",
    "                  OutputMV:f'{MARXAN_FOLDER}/{userInputFile.OUTPUTDIR}/{userInputFile.SCENNAME}_mvbest.csv', \n",
    "                  OutputSum:f'{MARXAN_FOLDER}/{userInputFile.OUTPUTDIR}/{userInputFile.SCENNAME}_sum.csv', \n",
    "                  OutputSsoln:f'{MARXAN_FOLDER}/{userInputFile.OUTPUTDIR}/{userInputFile.SCENNAME}_ssoln.csv',\n",
    "                  OutputSolutionsMatrix:f'{MARXAN_FOLDER}/{userInputFile.OUTPUTDIR}/{userInputFile.SCENNAME}_solutionsmatrix.csv'}\n",
    "    if model in model_dict.keys():\n",
    "        model_list = CreateListModelFromFile(model_dict[model], model)\n",
    "        model_df = pd.DataFrame([s.__dict__ for s in model_list])\n",
    "        CreateFileFromDF(model_dict[model],model_df,model)\n",
    "    return model_df\n",
    "    \n",
    "def modify_pu_status(MARXAN_FOLDER, MARXAN_INPUTDATA, percentage= 0.3, lock_out =True):\n",
    "    userInputFile = readInput(MARXAN_FOLDER, MARXAN_INPUTDATA)\n",
    "    pu = validateFile(MARXAN_FOLDER,'input.dat',planningUnits)\n",
    "\n",
    "    randomlist = []\n",
    "    if lock_out:\n",
    "        for i in range(0,int(percentage*len(pu))):\n",
    "            n = random.randrange(2,4,1)\n",
    "            randomlist.append(n)\n",
    "    else:\n",
    "        for i in range(0,int(percentage*len(pu))):\n",
    "            n = random.randrange(2,3,1)\n",
    "            randomlist.append(n)\n",
    "        \n",
    "\n",
    "    l = [0] * (len(pu) - len(randomlist))\n",
    "    randomlist.extend(l)\n",
    "    random.shuffle(randomlist)\n",
    "\n",
    "    pu['status'] = randomlist\n",
    "    CreateFileFromDF(f'{MARXAN_FOLDER}/{userInputFile.INPUTDIR}/pumock.dat',pu,planningUnits)\n",
    "    return pu"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "638b0add",
   "metadata": {},
   "source": [
    "## BLM Calibration"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "id": "cf28aef1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def createBlmProject(MARXAN_FOLDER: str, MARXAN_INPUTDATA:str, grid_file_path:str) -> list:\n",
    "    # copytree(f'{path}', f'{path}/blm')\n",
    "    # BLM values to start calibration\n",
    "    # Rule of thumb: Z = pu_area*[0.01,0.1,1,10,100]\n",
    "    \n",
    "    ### RUN MARXAN several times\n",
    "    blm_range= [0.001,0.01,0.1,1,10,100]\n",
    "    pu_area = gpd.read_file(grid_file_path)\n",
    "    area = pu_area['geometry'].map(lambda p: p.area / 10**6).mean()\n",
    "    blm_values = [element * math.sqrt(area) for element in blm_range]\n",
    "    blm_dict = dict(zip(blm_range, blm_values))\n",
    "    \n",
    "    blm_folder=[]\n",
    "\n",
    "    for blm in blm_range:\n",
    "        print(f'\\033[1m --> Running BLM_{blm}...\\033[0m')\n",
    "        \n",
    "        ## Create a folder for each BLM run at the same level as the original folder, \n",
    "        ## remove  folder if there is a previous one\n",
    "        ## copy all  files ecxcpet outputs\n",
    "        if os.path.exists(f'{os.path.dirname(MARXAN_FOLDER)}/BLM_{blm}'):\n",
    "            rmtree(f'{os.path.dirname(MARXAN_FOLDER)}/BLM_{blm}')\n",
    "        copytree(MARXAN_FOLDER, f'{os.path.dirname(MARXAN_FOLDER)}/BLM_{blm}',ignore=ignore_patterns('output_*'))\n",
    "        \n",
    "            \n",
    "        ## Read input files\n",
    "        InputFile = DatFile(f'{os.path.dirname(MARXAN_FOLDER)}/BLM_{blm}/{MARXAN_INPUTDATA}')\n",
    "        InputFile.read()\n",
    "        userInputFile = inputDatFile.from_dat(InputFile.data)\n",
    "\n",
    "        ## Modify for BLM calculations and save as new input.dat\n",
    "        userInputFile.BLM = blm_dict[blm]\n",
    "        userInputFile.OUTPUTDIR = 'output'\n",
    "        userInputFile.NUMREPS = 10\n",
    "        userInputFile.VERBOSITY = 0\n",
    "\n",
    "        userInputFile_df = pd.DataFrame.from_dict(userInputFile.__dict__, orient='index')\n",
    "        userInputFile_df.drop('BLOCKDEFNAME', inplace=True)\n",
    "\n",
    "        CreateFileFromDF(f'{os.path.dirname(MARXAN_FOLDER)}/BLM_{blm}/{MARXAN_INPUTDATA}',userInputFile_df, inputDatFile)\n",
    "        if not os.path.exists(f'{MARXAN_FOLDER}/{userInputFile.OUTPUTDIR}'):\n",
    "            os.mkdir(f'{MARXAN_FOLDER}/{userInputFile.OUTPUTDIR}')\n",
    "\n",
    "        blm_folder.append(f'BLM_{blm}')\n",
    "        os.chmod(f'{os.path.dirname(MARXAN_FOLDER)}/BLM_{blm}/marxan', 0o755)\n",
    "        execute_marxan(f'{os.path.dirname(MARXAN_FOLDER)}/BLM_{blm}')\n",
    "    \n",
    "    return blm_folder, blm_values\n",
    "\n",
    "def plotClumping(MARXAN_FOLDER: str, MARXAN_INPUTDATA : str,grid_file_path:str):\n",
    "    blm_folder = [filename for filename in os.listdir(os.path.dirname(MARXAN_FOLDER)) if filename.startswith(\"BLM\")]\n",
    "    fig = plt.figure(figsize=(10,10))\n",
    "\n",
    "    for idx, folder in enumerate(blm_folder):\n",
    "        axn = fig.add_subplot(321+idx)\n",
    "        solution = validateFile(f'{os.path.dirname(MARXAN_FOLDER)}/{folder}',MARXAN_INPUTDATA, OutputRun)\n",
    "        pu_area = gpd.read_file(grid_file_path)\n",
    "        puid_list = ['PUID','PU_ID','puid','pu_id']\n",
    "        pu_col = list(set(pu_area.columns) & set(puid_list))[0]\n",
    "        solution_grid = pu_area.merge(solution,left_on=f'{pu_col}',right_on = 'PUID',how='inner')\n",
    "        solution_grid.plot(ax=axn,column='SOLUTION', legend=True)\n",
    "        blm = readInput(f'{os.path.dirname(MARXAN_FOLDER)}/{folder}', MARXAN_INPUTDATA).BLM\n",
    "        axn.set_title(f'BLM = {round(blm,4)}')\n",
    "        \n",
    "    return plt.show()\n",
    "\n",
    "def BLM_calibration(MARXAN_FOLDER: str, MARXAN_INPUTDATA :str,grid_file_path: str, Plot: bool = True)-> float:\n",
    "    blm_df =pd.DataFrame(columns=['folder','blm'])\n",
    "    blm_folder, blm_values = createBlmProject(MARXAN_FOLDER, MARXAN_INPUTDATA,grid_file_path)\n",
    "    blm_df['folder'] = blm_folder\n",
    "    blm_df['blm']= blm_values\n",
    "\n",
    "    for blm in blm_folder:\n",
    "        solution = validateFile(f'{os.path.dirname(MARXAN_FOLDER)}/{blm}',MARXAN_INPUTDATA, OutputRun)\n",
    "        summary = validateFile(f'{os.path.dirname(MARXAN_FOLDER)}/{blm}',MARXAN_INPUTDATA, OutputSum)\n",
    "        best =summary.loc[summary.loc[:]['Score'].idxmin(),'Run_Number']\n",
    "        cost=summary.loc[best-1,'Cost']\n",
    "        blm_df.loc[blm_df['folder']==blm,'cost']=cost\n",
    "    \n",
    "        pu_area = gpd.read_file(grid_file_path)\n",
    "        puid_list = ['PUID','PU_ID','puid','pu_id']\n",
    "        pu_col = list(set(pu_area.columns) & set(puid_list))[0]\n",
    "        \n",
    "        solution_grid = pu_area.merge(solution,left_on=f'{pu_col}',right_on = 'PUID',how='inner')\n",
    "        blm_df.loc[blm_df['folder']==blm,'boundary_length']=solution_grid.dissolve(by='SOLUTION')['geometry'].length[1] ## perimeter in m\n",
    "    \n",
    "    ###Curve with no fit\n",
    "    y = blm_df['boundary_length']\n",
    "    x = blm_df['cost']\n",
    "    kn = kneed.KneeLocator(x, y, curve='convex', direction='decreasing')\n",
    "    best_blm = round(blm_df.loc[blm_df['cost']==kn.knee,'blm'].values[0],4)\n",
    "    #blm_df.loc[blm_df['cost']==kn.knee,'folder'].values[0] #in which folder\n",
    "    \n",
    "#   ##Curve fit with polynomial\n",
    "#     y = blm_df['boundary_length'].values\n",
    "#     x = blm_df['cost'].values\n",
    "#     fit = np.polyfit(x, y, 3)\n",
    "#     fit_equation = fit[0]*x**3 + fit[1]*x**2 + fit[2]*x +fit[3]\n",
    "#     kn_p = kneed.KneeLocator(x, fit_equation, curve='convex', direction='decreasing')\n",
    "#     #best_blm_poly = blm_df.loc[blm_df['cost']==kn.knee,'blm'].values[0]\n",
    "#     best_blm_poly = kn.knee\n",
    "\n",
    "    \n",
    "#    ##Curve fit with spline\n",
    "#     blm_df = blm_df.sort_values('cost')\n",
    "#     y_sp = blm_df['boundary_length'].values\n",
    "#     x_sp = blm_df['cost'].values\n",
    "#     spl = splrep(x_sp,y_sp)\n",
    "#     y_spl = splev(x_sp,spl)\n",
    "#     kn_sp = kneed.KneeLocator(x_sp, y_spl, curve='convex', direction='decreasing')\n",
    "#     #best_blm_sp = blm_df.loc[blm_df['cost']==kn.knee,'blm'].values[0]\n",
    "#     best_blm_sp = kn.knee\n",
    "\n",
    "    \n",
    "#     print(f'The optimun BLM is poly= {best_blm_poly}, spl = {best_blm_sp}')\n",
    "    print(f'The optimun BLM is {best_blm}')\n",
    "    \n",
    "    if Plot==True:\n",
    "        fig = plt.figure(figsize=(5,5))\n",
    "        plt.xlabel('cost')\n",
    "        plt.ylabel('boundary length')\n",
    "        plt.plot(x, y, 'bo-')\n",
    "        for xi in blm_df.cost:\n",
    "             plt.text(blm_df.loc[(blm_df['cost']==xi),'cost'].values[0], \n",
    "                      blm_df.loc[(blm_df['cost']==xi),'boundary_length'].values[0],\n",
    "                      round(blm_df.loc[(blm_df['cost']==xi),'blm'].values[0],4))\n",
    "        \n",
    "#         plt.plot(x_sp,y_spl,'--')\n",
    "#         plt.plot(x, fit_equation,color = 'r',alpha = 0.5, label = 'Polynomial fit')\n",
    "#         plt.vlines(kn_p.knee, plt.ylim()[0], plt.ylim()[1], linestyles='dashed')\n",
    "#         plt.vlines(kn_sp.knee, plt.ylim()[0], plt.ylim()[1], linestyles='dashed')\n",
    "        plt.vlines(kn.knee, plt.ylim()[0], plt.ylim()[1], linestyles='dashed')\n",
    "        \n",
    "    return best_blm"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e582135",
   "metadata": {},
   "source": [
    "## Cluster 5 most different solutions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c7a928ab",
   "metadata": {},
   "outputs": [],
   "source": [
    "def clusterSolutions(MARXAN_FOLDER, MARXAN_INPUTDATA,k=5):\n",
    "    \"\"\"\n",
    "    Returns a list of the 5 most different solutions\n",
    "    \"\"\"\n",
    "    \n",
    "    # Open solutions matrix file\n",
    "    userInputFile = readInput(MARXAN_FOLDER, MARXAN_INPUTDATA)\n",
    "    userSolMat_df = validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, OutputSolutionsMatrix)\n",
    "    solmat = userSolMat_df.drop(columns=userSolMat_df.columns[1]).join(userSolMat_df[userSolMat_df.columns[1]].apply(pd.Series))\n",
    "    \n",
    "    solmat = solmat.loc[:,solmat.columns != 'SolutionsMatrix']\n",
    "    \n",
    "    # Create distance matrix with Jaccard similarity\n",
    "    dist_mat = linkage(solmat, method='average',metric='jaccard')\n",
    "    \n",
    "    # Find k clusters\n",
    "    # print(f'Building cluster of {k} most different solutions')\n",
    "    groups = fcluster(dist_mat, k, criterion='maxclust')\n",
    "    \n",
    "    # Get best solution per cluster (solution with the lowest Score) from sum table\n",
    "    summary = validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, OutputSum)\n",
    "    best =summary.loc[summary.loc[:]['Score'].idxmin(),'Run_Number']\n",
    "    print(f'Overall best solution is {best}')\n",
    "    \n",
    "    bestlist =[]\n",
    "    for i in range(k):\n",
    "        g = np.where(groups == i+1)[0]\n",
    "        sol = summary.loc[summary.loc[g]['Score'].idxmin(),'Run_Number']\n",
    "#         print(f'Group {i+1} best solution = {sol}')\n",
    "        bestlist.append(sol)\n",
    "\n",
    "#     See figure\n",
    "#     plt.figure(figsize=(10, 7))\n",
    "#     plt.scatter(clust[:,0], clust[:,1], c=cluster.labels_, cmap='rainbow')\n",
    "    \n",
    "    return bestlist"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4dd96094",
   "metadata": {},
   "source": [
    "## Gap Analysis"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d0b7805c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def gapAnalysis(MARXAN_FOLDER,MARXAN_INPUTDATA,premarxan =True, postmarxan=True):\n",
    "    \"\"\"This function analyses the protected amount of each species\n",
    "    and returns a DataFrame with the values needed to construct the Gap Analysis.\n",
    "    Calculates for each feature\n",
    "    - amount_total: Total area of a feature in planning area\n",
    "    - amount_target: Target area to achieve (target column renamed)\n",
    "    - amount_lock: Area of feature inside lock in areas\n",
    "    - amount_best: Area of feature slected from the best solution\n",
    "    - prop: proportion of total area to achieve \n",
    "    - prop_lock: proportion of area locked in\n",
    "    \"\"\"\n",
    "    ## Validate all input files\n",
    "    userInputFile = readInput(MARXAN_FOLDER,MARXAN_INPUTDATA)\n",
    "    pu = validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, planningUnits)\n",
    "    spec = validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, conservationFeature)\n",
    "    puvsp = validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, planningUnitVSConservationFeatureV)\n",
    "    \n",
    "   # Calculate total amount of the feature in the planning area and merge with spec.dat file\n",
    "    total = puvsp.groupby(['species']).sum().reset_index()\n",
    "    df=spec.merge(total[['species','amount']], \n",
    "                       left_on = 'id', \n",
    "                       right_on = 'species', \n",
    "                       how='inner').drop(columns=['species']).rename(columns={'amount':'amount_total'})\n",
    "    \n",
    "    # Calculate the amount to achieve (target-as area or prop- as %) from spec.dat\n",
    "    # if 'prop' is the available column: calculate 'target' as amount_total* prop \n",
    "    # if 'target' is the available column: calculate 'prop' as target/amount_total\n",
    "    if 'prop' in spec.columns:\n",
    "        df['target']=df['amount_total']*df['prop']\n",
    "        df['amount_target']=df['amount_total']*df['prop']\n",
    "    \n",
    "    else: # 'target' in spec.columns\n",
    "        df['prop']=df['target']/df['amount_total']\n",
    "        df.rename(columns={'target':'amount_target'})\n",
    "    \n",
    "    \n",
    "    # Calculate the feature amount in locked in planning units\n",
    "    # Get PUID that have been locked-in (status = 2 in pu.dat)\n",
    "    pulock = pu[pu['status']==2].id\n",
    "    lockin = puvsp[puvsp.pu.isin(pulock)].groupby(['species']).sum().reset_index()\n",
    "    df = df.merge(lockin[['species','amount']], \n",
    "                                left_on = 'id', \n",
    "                                right_on = 'species', \n",
    "                                how='left').drop(columns=['species']).rename(columns={'amount':'amount_lock'})\n",
    "\n",
    "    # fill NaN with 0\n",
    "    df = df.fillna(0)\n",
    "    # Porportion of target that is in locked in areas (PA) \n",
    "    df['prop_lock']=(df['amount_lock']/df['amount_total'])    \n",
    "    \n",
    "    # Target met\n",
    "    df['target_met_pre']= np.where((df['prop_lock']/df['prop'])*100 > userInputFile.MISSLEVEL, 'yes', 'no')\n",
    "    \n",
    "    \n",
    "    if postmarxan:\n",
    "        # Calculate amount protected in best solution\n",
    "        # Validate OutputMV\n",
    "        mvbest = validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, OutputMV)\n",
    "    \n",
    "        # Get PUID that have been selected\n",
    "        df = df.merge(mvbest, \n",
    "               left_on = 'id', \n",
    "               right_on = 'Conservation_Feature', \n",
    "               how='left').drop(columns=['Conservation_Feature','Feature_Name'])\n",
    "\n",
    "        # Add proportion best to table\n",
    "        df['prop_best']=(df['Amount_Held']/df['amount_total'])\n",
    "        \n",
    "        # Best solution - locked species amount (for additive plotting)\n",
    "        df['prop_best_minus_lock']=(df['prop_best']-df['prop_lock'])\n",
    "    \n",
    "    return df.drop(columns=['type','spf','target2','targetocc','sepnum','sepdistance'])\n",
    "\n",
    "\n",
    "def plotGap(data, title,ax, post_marxan=True):\n",
    "    width = 0.8\n",
    "        \n",
    "    if post_marxan:\n",
    "        sort_propf = data.sort_values(\"prop_best\")\n",
    "        labels = sort_propf.name if sort_propf.name.dtype == 'str' else sort_propf.id.apply(str)\n",
    "        x_pos = [i for i, _ in enumerate(labels)]\n",
    "        target = sort_propf.prop\n",
    "        ax.plot(target,labels,color='red')    \n",
    "        ax.barh(labels,sort_propf['prop_best_minus_lock'], width, left=sort_propf['prop_lock'],\n",
    "            label='marxan',color='#00cccc', edgecolor='#00cccc', align='center')\n",
    "        ax.barh(labels, sort_propf.prop_lock, width, label='locked/protected',color='#617f9d', edgecolor='#617f9d', align='center')\n",
    "    \n",
    "    else:\n",
    "        sort_propi= data.sort_values(\"prop_lock\")\n",
    "        labels = sort_propi.name if sort_propi.name.dtype == 'str' else sort_propi.id.apply(str)\n",
    "        x_pos = [i for i, _ in enumerate(labels)]\n",
    "        target = sort_propi.prop\n",
    "        ax.plot(target,labels,color='red')    \n",
    "        ax.barh(labels, sort_propi['prop_lock'], width, label='locked/protected',color='#617f9d', edgecolor='#617f9d', align='center')   \n",
    "            \n",
    "    ax.set_xlabel('Percentage protected')\n",
    "    ax.set_ylabel('Species')\n",
    "    ax.set_title(title)\n",
    "    ax.legend(loc ='upper left')\n",
    "    ax.set_xlim([0,1])\n",
    "    reduced_labels = [word[:15] for word in labels] if labels.dtype == 'str' else df.id\n",
    "    plt.setp(ax, yticks=x_pos, yticklabels=reduced_labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9504e469",
   "metadata": {},
   "source": [
    "## Unmet targets pipeline  \n",
    "\n",
    "### Identify unmet targets: \n",
    "    File output_mvbest, columns 'Target Met' + 'MPM' (Minimum Proportion Met)\n",
    "\n",
    "### Workflow for unmet solutions:\n",
    "    If the feature is not met but by very little --> mark as met or decrease MISSLEVEL \n",
    "    Else:\n",
    "        If the feature is in locked-out planning units --> Send warning/mark as met/rethink problem\n",
    "        Else:\n",
    "            If the feature is in a high cost area --> Increase SPF\n",
    "            Else:\n",
    "                The feature has low range and is isolated --> Increase SPF"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0625e94c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def locatePU(MARXAN_FOLDER:str,MARXAN_INPUTDATA:str,pus_of_interest=None, feature=None) -> dict:\n",
    "    \"\"\" Check if feature is in an area of interest\n",
    "    \"\"\"\n",
    "    userInputFile = readInput(MARXAN_FOLDER,MARXAN_INPUTDATA)\n",
    "    puvsp = validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, planningUnitVSConservationFeatureV)\n",
    "    \n",
    "    feature_in_pu=puvsp[(puvsp['species']==feature) & (puvsp.pu.isin(pus_of_interest))]\n",
    "    \n",
    "    if len(feature_in_pu)== 0: \n",
    "        percentage_in_pu = 0\n",
    "    else:\n",
    "        amount_total = puvsp[puvsp['species']==feature].groupby(['species']).sum('amount').reset_index().amount.values[0]\n",
    "        amount_in_pu= feature_in_pu.groupby('species').sum('amount').reset_index().amount.values[0]\n",
    "        percentage_in_pu = round((amount_in_pu/amount_total)*100,2)\n",
    "    return percentage_in_pu\n",
    "\n",
    "def unmetDecisionTree(MARXAN_FOLDER, MARXAN_INPUTDATA,\n",
    "                      lock_out_limit=50, \n",
    "                      high_cost_quantile= 0.7, \n",
    "                      hcost_limit=20):\n",
    "    \n",
    "    \"\"\"Follow the decision flow:\n",
    "    - Check if the target is missed by little (already considering the defined misslevel)\n",
    "    - Check if the feature is in a locked-out area \n",
    "    - Check if the feature is in a high cost area\n",
    "    - Check if the feature is very isolated or the range of the feature is very small\n",
    "    \n",
    "    Parameteres:\n",
    "    - threshold met: when to consider a target as met (dy default the misslevel set at input.dat)\n",
    "    - lock_out_limit: amount of the feature pu's that are in locked out areas that trigger response\n",
    "    - high_cost_quantile: decide what is high cost\n",
    "    - hcost_limit: amount of the feature pu's that are in high cost areas that trigger response\n",
    "    \"\"\"\n",
    "    \n",
    "    # Validate and read files\n",
    "    userInputFile = readInput(MARXAN_FOLDER,MARXAN_INPUTDATA)\n",
    "    pu = validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, planningUnits)\n",
    "    puvsp = validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, planningUnitVSConservationFeatureV)\n",
    "    mvbest= validateFile(MARXAN_FOLDER,MARXAN_INPUTDATA, OutputMV)\n",
    "    \n",
    "    df = mvbest[['Conservation_Feature','Target_Met','MPM']].copy()\n",
    "    threshold_met = userInputFile.MISSLEVEL\n",
    "    \n",
    "    if len(df.loc[df['Target_Met']=='no','Conservation_Feature'])==0:\n",
    "        unmet = {'None':'All targets met'}\n",
    "    else: \n",
    "        for feature in list(df.loc[df['Target_Met']=='no','Conservation_Feature'].values):\n",
    "        \n",
    "            # 1. Close to target \n",
    "            # Solution: \n",
    "            # - Mark as met: Modify output_mvbest.csv (Target Met) MPM: minimum proportion met\n",
    "            # - Decrease MISSLEVEL (input.dat)\n",
    "            if (df.loc[df['Conservation_Feature']==feature,'MPM']>=threshold_met-(threshold_met*1/100)).bool():\n",
    "                df.loc[df['Conservation_Feature']==feature,'Eval']='Close to target (1% away of misslevel)'\n",
    "\n",
    "            else:\n",
    "        \n",
    "            # 2. In lock-out areas (re-think problem)\n",
    "            # Solution: \n",
    "            # - Rethink problem (Assume not met)\n",
    "            # - Unblock lock-out pu: Modifir pu.dat (status)\n",
    "        \n",
    "                pu_excluded = list(pu[pu['status']==3].id) \n",
    "                excluded_per = locatePU(MARXAN_FOLDER,MARXAN_INPUTDATA,pus_of_interest=pu_excluded, feature=feature)\n",
    "                if excluded_per > lock_out_limit: df.loc[df['Conservation_Feature']==feature,'Eval'] = f'{excluded_per} % in locked-out areas'\n",
    "            \n",
    "                else:\n",
    "                        \n",
    "            # 3. In high cost areas (increase SPF)\n",
    "            # Solution: \n",
    "            # - Increase SPF: Modify spec.dat (spf)\n",
    "        \n",
    "                    pu_hcost = list(pu[pu['cost']>pu['cost'].quantile([high_cost_quantile], interpolation='nearest').values[0]].id)\n",
    "                    hcost_per = locatePU(MARXAN_FOLDER,MARXAN_INPUTDATA,pus_of_interest=pu_excluded, feature=feature)\n",
    "                    if hcost_per > hcost_limit: df.loc[df['Conservation_Feature']==feature,'Eval'] = f'{hcost_per} % in high cost areas'\n",
    "                \n",
    "                    else:\n",
    "        \n",
    "            # 4. Small range or isolated \n",
    "            # Solution:\n",
    "            # - Increase SPF: Modify spec.dat (spf)\n",
    "                        feat_range = round(puvsp[puvsp['species']==feature].count().pu/len(pu)*100,2)\n",
    "                        df.loc[df['Conservation_Feature']==feature,'Eval'] = f'Small range ({feat_range} % of planning area) or isolated'     \n",
    "        \n",
    "            unmet = dict(zip(df[df['Target_Met']=='no'].Conservation_Feature, df[df['Target_Met']=='no'].Eval))\n",
    "    return unmet"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7eb0980d",
   "metadata": {},
   "source": [
    "### Difference Map"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "id": "75b336de",
   "metadata": {},
   "outputs": [],
   "source": [
    "def diffMap(SCEN1_PATH: str,SCEN2_PATH: str, show_count :bool)-> dict:\n",
    "# 1. Read files (input.dat, pu.dat, pu_grid)\n",
    "    MARXAN_INPUTDATA = 'input.dat'\n",
    "    # Input files\n",
    "    input1 = readInput(SCEN1_PATH,MARXAN_INPUTDATA)\n",
    "    input2 = readInput(SCEN2_PATH,MARXAN_INPUTDATA)\n",
    "    \n",
    "    #pu.dat files\n",
    "    pu1 = validateFile(SCEN1_PATH,MARXAN_INPUTDATA, planningUnits)\n",
    "    pu2 = validateFile(SCEN2_PATH,MARXAN_INPUTDATA, planningUnits)\n",
    "    \n",
    "    ## Frequency of solutions output file\n",
    "    s1 = validateFile(SCEN1_PATH,MARXAN_INPUTDATA, OutputSsoln)\n",
    "    s2 = validateFile(SCEN2_PATH,MARXAN_INPUTDATA, OutputSsoln)\n",
    "\n",
    "    ## Number of runs\n",
    "    n1 = int(input1.NUMREPS)\n",
    "    n2 = int(input2.NUMREPS)\n",
    "    \n",
    "# 2. Rename fields and merge with grid \n",
    "    # freq = the total number of times a pu has been selected in all solutions\n",
    "    diff= s1.rename(columns={'number':'freq1'})\n",
    "    diff['freq2'] = s2.number\n",
    "\n",
    "    # rel_freq = freq/number of solutions (different scenarios may have different solution number)\n",
    "    diff['rel_freq1'] = round(diff.freq1/n1,2)\n",
    "    diff['rel_freq2'] = round(diff.freq2/n2,2)\n",
    "\n",
    "    # diff = selection of scenario 1 - scenario 2\n",
    "    # dif_abs = absolute difference \n",
    "    diff['diff'] = diff.rel_freq1  - diff.rel_freq2\n",
    "    diff['diff_abs'] = diff['diff'].abs()\n",
    "\n",
    "    # pu.dat get status\n",
    "    pu1 = pu1.rename(columns={'status':'status1'})\n",
    "    pu1['status2'] = pu2.status\n",
    "\n",
    "# 3. Assign categories\n",
    "    # Never selected in S1 & S2\n",
    "    never = list(diff[(diff['rel_freq1'] == 0)   & (diff['rel_freq2'] == 0)].planning_unit.values)\n",
    "\n",
    "    # Locked-in /Always\n",
    "    # status 2 in pu.dat should give rel_freq=1, but that is not the case. TO DO: understand why\n",
    "    lock1 = list(diff[(diff['rel_freq1'] == 1)].planning_unit.values)\n",
    "    lock2 = list(diff[(diff['rel_freq2'] == 1)].planning_unit.values)\n",
    "    always = list(set(lock1) & set(lock2)) # Always selected in both\n",
    "    S1_always = list(set(lock1) - set(always)) # always selected in S1\n",
    "    S2_always = list(set(lock2) - set(always)) # always selected in S2\n",
    "\n",
    "    # Always selected in S1 & S2\n",
    "    #always = list(diff[(diff['rel_freq1'] == 1) & (diff['rel_freq2'] == 1)].planning_unit.values)\n",
    "\n",
    "    # Only selected in S1, but below 1\n",
    "    S1_only = list(diff[(diff['rel_freq1'] != 0 ) & (diff['rel_freq2'] == 0)].planning_unit.values)\n",
    "    S1_only = list(set(S1_only) - set(S1_always))\n",
    "\n",
    "    # Only selected in S2, but below 1\n",
    "    S2_only = list(diff[(diff['rel_freq1'] == 0) & (diff['rel_freq2'] != 0)].planning_unit.values)\n",
    "    S2_only = list(set(S2_only) - set(S2_always))\n",
    "    \n",
    "    # Selected in both with comparable frequency (difference between -0.1 and 0.1)\n",
    "    both_comparable = list(diff[(diff['rel_freq1'] != 0) & \n",
    "                            (diff['rel_freq1'] < 1) &\n",
    "                            (diff['rel_freq2'] != 0) & \n",
    "                            (diff['rel_freq2'] < 1) & \n",
    "                            (diff['diff_abs'] <= 0.1)].planning_unit.values)\n",
    "\n",
    "    # Selected in both with higher frequency in S1 (difference > 0.1)\n",
    "    both_higherS1 = list(diff[(diff['rel_freq1'] != 0) & \n",
    "                            (diff['rel_freq1'] < 1) &\n",
    "                            (diff['rel_freq2'] != 0) & \n",
    "                            (diff['rel_freq2'] < 1)  & \n",
    "                            (diff['diff'] > 0.1)].planning_unit.values)\n",
    "\n",
    "    # Selected in both with higher frequency in S2 (difference < -0.1)\n",
    "    both_higherS2 = list(diff[(diff['rel_freq1'] != 0) & \n",
    "                            (diff['rel_freq1'] < 1) &\n",
    "                            (diff['rel_freq2'] != 0) & \n",
    "                            (diff['rel_freq2'] < 1)  & \n",
    "                            (diff['diff'] < -0.1)].planning_unit.values)\n",
    "    \n",
    "    # create dict of categories\n",
    "    categories = {'never': never,\n",
    "         'always':always,\n",
    "         'S1_always':S1_always,\n",
    "         'S2_always':S2_always,\n",
    "         'S1_only':S1_only,\n",
    "         'S2_only':S2_only,\n",
    "         'both_comparable':both_comparable,\n",
    "         'both_higherS1':both_higherS1,\n",
    "         'both_higherS2':both_higherS2}\n",
    "    \n",
    "    diff['category']= 'NotAssigned'\n",
    "    for key in categories.keys():\n",
    "        diff.loc[diff['planning_unit'].isin(categories[key]),'category'] = key\n",
    "        \n",
    "# 4. Sanity checks\n",
    "# All pu's are assigned to one category\n",
    "    if show_count:\n",
    "        all_pu= []\n",
    "        for i in categories.keys():\n",
    "            all_pu = all_pu + categories[i]\n",
    "            print(f\"there are {len(categories[i])} pu's assigned to {i}\")\n",
    "        print(f\"there are {len(diff)-len(all_pu) } pu's not assigned\")\n",
    "    \n",
    "    # Covert to dict (this format can be more elaborate depending on FE requirements)\n",
    "    diff_dict =diff[['planning_unit','category','diff','diff_abs']].to_dict()\n",
    "    \n",
    "    return diff_dict\n",
    "\n",
    "def plotDiffMap(diff_dict: dict,GRID_PATH: str, solid: bool):\n",
    "    # grid file\n",
    "    pu_grid = gpd.read_file(f'{GRID_PATH}')\n",
    "    puid_list = ['PIUD','PU_ID','puid','pu_id']\n",
    "    for option in puid_list:\n",
    "        for col_name in pu_grid.columns:\n",
    "            if option in col_name:\n",
    "                pu_col = col_name\n",
    "    \n",
    "    # convert dict to dataframe\n",
    "    diff = pd.DataFrame.from_dict(diff_dict,orient='columns')\n",
    "    \n",
    "    # merge with pu grid\n",
    "    diff = pu_grid[[f'{pu_col}','geometry']].merge(diff,left_on=f'{pu_col}',right_on ='planning_unit',how='inner')\n",
    "    \n",
    "    # Plot as solid colors or frequency \n",
    "    diff.plot(column = 'category',legend =True,figsize =(10,10))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "da085fc4",
   "metadata": {},
   "source": [
    "### FPF Calibration"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0fc98b53",
   "metadata": {},
   "outputs": [],
   "source": [
    "def createFpfProject(MARXAN_FOLDER: str, MARXAN_INPUTDATA:str, unmet_dict: dict) -> list:\n",
    "    \n",
    "    ### RUN MARXAN several times\n",
    "    fpf_range= [1,2,5,10,20,50,100]\n",
    "    fpf_folder=[]\n",
    "\n",
    "    for fpf in fpf_range:\n",
    "        print(f'\\033[1m --> Running FPF_{fpf}...\\033[0m')\n",
    "        \n",
    "        ## Create a folder for each FPF run at the same level as the original folder, \n",
    "        ## remove  folder if there is a previous one\n",
    "        ## copy all  files except outputs\n",
    "        if os.path.exists(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}'):\n",
    "            rmtree(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}')\n",
    "        copytree(MARXAN_FOLDER, f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}',ignore=ignore_patterns('output_*'))\n",
    "        \n",
    "            \n",
    "        ## Read input files\n",
    "        userInputFile = readInput(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}', MARXAN_INPUTDATA)\n",
    "        userInputFile.NUMREPS = 10\n",
    "        userInputFile.VERBOSITY = 0\n",
    "        saveInput(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}', MARXAN_INPUTDATA,userInputFile)\n",
    "        \n",
    "        spec = validateFile(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}',MARXAN_INPUTDATA, conservationFeature)\n",
    "        \n",
    "        unmet = list(unmet_dict.keys())\n",
    "        for feature in unmet:\n",
    "            spec.loc[spec['id'] == feature,'spf'] = (spec[spec['id'] == feature].spf)*fpf\n",
    "        CreateFileFromDF(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}/{userInputFile.INPUTDIR}/{userInputFile.SPECNAME}',spec, conservationFeature) \n",
    "        \n",
    "        if not os.path.exists(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}/{userInputFile.OUTPUTDIR}'):\n",
    "            os.mkdir(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}/{userInputFile.OUTPUTDIR}')\n",
    "\n",
    "        fpf_folder.append(f'FPF_{fpf}')\n",
    "        os.chmod(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}/marxan', 0o755)\n",
    "        execute_marxan(f'{os.path.dirname(MARXAN_FOLDER)}/FPF_{fpf}')\n",
    "    \n",
    "    return fpf_folder\n",
    "\n",
    "def FPF_calibration(MARXAN_FOLDER: str, MARXAN_INPUTDATA :str, unmet_dict: dict, Plot: bool = True)-> float:\n",
    "    fpf_df =pd.DataFrame(columns=['folder','multiplier','missed_targets'])\n",
    "    fpf_folder = createFpfProject(MARXAN_FOLDER, MARXAN_INPUTDATA, unmet_dict)\n",
    "    fpf_df['folder'] = fpf_folder\n",
    "    fpf_df['multiplier']= [int(i.split('_')[1]) for i in fpf_folder]\n",
    "\n",
    "    for fpf in fpf_folder:\n",
    "        # using mvbest\n",
    "        #mvbest = validateFile(f'{os.path.dirname(MARXAN_FOLDER)}/{fpf}',MARXAN_INPUTDATA, OutputMV)\n",
    "        #fpf_df.loc[fpf_df['folder']==fpf,'missed_targets']= len(mvbest[mvbest['Target_Met'] == 'no'])\n",
    "        \n",
    "        # using an average of all the runs\n",
    "        summary = validateFile(f'{os.path.dirname(MARXAN_FOLDER)}/{fpf}',MARXAN_INPUTDATA, OutputSum)\n",
    "        fpf_df.loc[fpf_df['folder']==fpf,'missed_targets']= summary['Missing_Values'].mean()\n",
    "    \n",
    "    ###Curve with no fit\n",
    "    y = fpf_df['missed_targets']\n",
    "    x = fpf_df['multiplier']\n",
    "\n",
    "    best_fpf = fpf_df.loc[fpf_df['missed_targets'] == min(fpf_df.missed_targets),'multiplier'].values[0]\n",
    "    \n",
    "    if Plot==True:\n",
    "        fig = plt.figure(figsize=(5,5))\n",
    "        plt.xlabel('FPF multiplier')\n",
    "        plt.ylabel('Missed targets')\n",
    "        plt.plot(x, y, 'bo-')\n",
    "        \n",
    "    return best_fpf"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac60b73a",
   "metadata": {},
   "source": [
    "## Input data model types"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "58d706cd",
   "metadata": {},
   "outputs": [],
   "source": [
    "class inputDatFile(BaseModel):\n",
    "    \"\"\"\n",
    "    This is the description of the input data clase base on marxan input file.\n",
    "    \"\"\"\n",
    "    \n",
    "    # General Parameters\n",
    "    VERSION: str = Field('0.1', title='Version', \n",
    "                         description='Type of input file')\n",
    "    BLM:  Optional[float] = Field(0., title='Boundary Length Modifier', \n",
    "                           description='Boundary Length Modifier')\n",
    "    PROP: float = Field(0., title='Starting Proportion', \n",
    "                            description='Proportion of planning units in initial reserve system')\n",
    "    RANDSEED: Optional[int] = Field(-1, title='Random Seed', \n",
    "                              description='Random seed number')\n",
    "    NUMREPS: int = Field(1, title='Repeat Runs', \n",
    "                             description='The number of repeat runs you wish to do')\n",
    "    BESTSCORE: Optional[int] = Field(0, title='Best Score Speedup', \n",
    "                               description='This variable tells Marxan not to keep track of the best score \\\n",
    "                                until it reaches a specified minimum level.')\n",
    "    \n",
    "    # Annealing Parameters\n",
    "    NUMITNS: int = Field(0, title='Number of Iterations', \n",
    "                         description='Number of iterations for annealing')\n",
    "    STARTTEMP: int = Field(1, title='Initial Temperature', \n",
    "                           description='Starting temperature for annealing')\n",
    "    COOLFAC: int = Field(0, title='Cooling Factor', \n",
    "                         description='Cooling factor for annealing')\n",
    "    NUMTEMP: int = Field(1, title='Temperature Decreases', \n",
    "                         description='Number of temperature decreases for annealing')\n",
    "    \n",
    "    # Cost Threshold\n",
    "    COSTTHRESH: Optional[float] = Field(0, title='Threshold', \n",
    "                              description='Cost threshold')\n",
    "    THRESHPEN1: Optional[float] = Field(0, title='Penalty Factor A', \n",
    "                              description='Size of cost threshold penalty')\n",
    "    THRESHPEN2: Optional[float] = Field(0, title='Penalty Factor B', \n",
    "                              description='Shape of cost threshold penalty')\n",
    "    \n",
    "    # Input Files\n",
    "    INPUTDIR: str = Field('input', title='Input Folder', \n",
    "                          description='User Defined Name of the folder containing input data files')\n",
    "    SPECNAME: str = Field('spec.dat', title='Species File Name', \n",
    "                          description='Name of Conservation Feature File')\n",
    "    PUNAME: str = Field('pu.dat', title='Planning Unit File Name', \n",
    "                        description='Name of Planning Unit File')\n",
    "    PUVSPRNAME: str = Field('puvspr2.dat', title='Planning Unit versus Species', \n",
    "                            description='Name of Planning Unit versus Conservation Feature File')\n",
    "    BOUNDNAME: str = Field('bound.dat', title='Boundary Length', \n",
    "                           description='Name of Boundary Length File')\n",
    "    BLOCKDEFNAME: Optional[str] = Field('blockdef.dat', title='Block Definitions', \n",
    "                              description='Name of Block Definition File')\n",
    "    \n",
    "    # Output Files\n",
    "    VERBOSITY: int =  Field(1, title='Screen Output', \n",
    "                            description='Amount of output displayed on the program screen')\n",
    "    MISSLEVEL: Optional[float] =  Field(1, title='Species missing proportion', \n",
    "                              description='Amount or target below which it is counted as ‘missing’')\n",
    "    OUTPUTDIR: str = Field('output', title='', \n",
    "                           description='User Defined Name of the folder in which to save output files')\n",
    "    SCENNAME: str = Field('Default_name', title='Scenario name', \n",
    "                          description='Scenario name for the saved output files')\n",
    "    SAVERUN: Optional[int] = Field(3, title='Save each run', \n",
    "                         description='Save each run? (0 = no)')\n",
    "    SAVEBEST: Optional[int] =  Field(3, title='Save the best run', \n",
    "                           description='Save the best run? (0 = no)')\n",
    "    SAVESUMMARY: Optional[int] =  Field(3, title='Save summary', \n",
    "                          description='Save summary information? (0 = no)')\n",
    "    SAVESCEN: Optional[int] =  Field(3, title='Save scenario', \n",
    "                           description='Save scenario information? (0 = no)')\n",
    "    SAVETARGMET: Optional[int] =  Field(3, title='Save targets met', \n",
    "                              description='Save targets met information? (0 = no)')\n",
    "    SAVESUMSOLN: Optional[int] =  Field(3, title='', \n",
    "                              description='Save summed solution information? (0 = no)')\n",
    "    SAVELOG: Optional[int] =  Field(1, title='Save summed solution', \n",
    "                          description='Save log files? (0 = no)')\n",
    "    SAVESNAPSTEPS: Optional[int] =  Field(0, title='Save snapshots', \n",
    "                                description='Save snapshots each n steps (0 = no)')\n",
    "    SAVESNAPCHANGES: Optional[int] =  Field(0, title='Save snapshots changes', \n",
    "                                  description='Save snapshots after every n changes (0 = no)')\n",
    "    SAVESNAPFREQUENCY: Optional[int] =  Field(0, title='Frequency of snapshots', \n",
    "                                    description='Frequency of snapshots if they are being used')\n",
    "    SAVESOLUTIONSMATRIX: Optional[int] =  Field(3, title='Frequency of snapshots', \n",
    "                                    description='Frequency of snapshots if they are being used')\n",
    "    \n",
    "    # Program control.\n",
    "    RUNMODE: int = Field(1, title='Run Options', \n",
    "                         description='User Defined The method Marxan uses to find solutions')\n",
    "    \n",
    "    ITIMPTYPE: int =  Field(1, title='Iterative Improvement', \n",
    "                            description='Iterative improvement type')\n",
    "    HEURTYPE: int =  Field(1, title='Heuristic', \n",
    "                           description='Heuristic type')\n",
    "    CLUMPTYPE: Optional[int] =  Field(0, title='Clumping Rule', \n",
    "                            description='Clumping penalty type')\n",
    "    \n",
    "    # class Config:\n",
    "    @validator('SAVERUN')\n",
    "    def SAVERUN_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'No file generated',\n",
    "                      1: 'save a file as .dat',\n",
    "                      2: 'save a file as .txt',\n",
    "                      3: 'save a file as .csv'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('SAVEBEST')\n",
    "    def SAVEBEST_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'No file generated',\n",
    "                      1: 'save a file as .dat',\n",
    "                      2: 'save a file as .txt',\n",
    "                      3: 'save a file as .csv'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('SAVESUMMARY')\n",
    "    def SAVESUMMMARY_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'No file generated',\n",
    "                      1: 'save a file as .dat',\n",
    "                      2: 'save a file as .txt',\n",
    "                      3: 'save a file as .csv'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('SAVESCEN')\n",
    "    def SAVESCEN_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'No file generated',\n",
    "                      1: 'save a file as .dat',\n",
    "                      2: 'save a file as .txt',\n",
    "                      3: 'save a file as .csv'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('SAVETARGMET')\n",
    "    def SAVETARGMET_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'No file generated',\n",
    "                      1: 'save a file as .dat',\n",
    "                      2: 'save a file as .txt',\n",
    "                      3: 'save a file as .csv'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('SAVESUMSOLN')\n",
    "    def SAVESUMSOLN_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'No file generated',\n",
    "                      1: 'save a file as .dat',\n",
    "                      2: 'save a file as .txt',\n",
    "                      3: 'save a file as .csv'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('SAVELOG')\n",
    "    def SAVELOG_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'No file generated',\n",
    "                      1: 'save a file as .dat',\n",
    "                      2: 'save a file as .txt',\n",
    "                      3: 'save a file as .csv'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('SAVESOLUTIONSMATRIX')\n",
    "    def SAVESOLUTIONSMATRIX_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'No file generated',\n",
    "                      1: 'save a file as .dat',\n",
    "                      2: 'save a file as .txt',\n",
    "                      3: 'save a file as .csv'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('RUNMODE')\n",
    "    def RUNMODE_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'Apply Simulated Annealing followed by a Heuristic',\n",
    "                      1: 'Apply Simulated Annealing followed by Iterative Improvement',\n",
    "                      2: 'Apply Simulated Annealing followed by a Heuristic, followed by Iterative',\n",
    "                      3: 'Use only a Heuristic',\n",
    "                      4: 'Use only Iterative Improvement',\n",
    "                      5: 'Use a Heuristic followed by Iterative Improvement',\n",
    "                      6: 'Use only Simulated Annealing'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('ITIMPTYPE')\n",
    "    def ITIMPTYPE_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {0: 'Normal Iterative Improvement',\n",
    "                      1: 'Two Step Iterative Improvement',\n",
    "                      2: '‘Swap’ Iterative Improvement',\n",
    "                      3: 'Normal Improvement followed by Two Step Iterative Improvement'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('HEURTYPE')\n",
    "    def HEURTYPE_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {-1:'Ignored',\n",
    "                      0: 'Richness',\n",
    "                      1: 'Greedy',\n",
    "                      2: 'Max Rarity',\n",
    "                      3: 'Best Rarity',\n",
    "                      4: 'Average Rarity',\n",
    "                      5: 'Sum Rarity',\n",
    "                      6: 'Product Irreplaceability',\n",
    "                      7: 'Summation Irreplaceability'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('VERBOSITY')\n",
    "    def VERBOSITY_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {-1:'Ignored',\n",
    "                      0: 'Silent Running',\n",
    "                      1: 'Results Only',\n",
    "                      2: 'General Progress',\n",
    "                      3: 'Detailed Progress'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    @validator('CLUMPTYPE')\n",
    "    def CLUMPTYPE_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {-1:'Ignored',\n",
    "                       0: 'Partial clumps do not count',\n",
    "                       1: 'Partial clumps count half',\n",
    "                       3: 'Graduated penalty'}\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "    \n",
    "    def to_dat(self):\n",
    "        \"\"\"\n",
    "        Gets a input.dat file and outputs a dict of parameters.\n",
    "\n",
    "        Args:\n",
    "            \n",
    "        Returns:\n",
    "            The data model converted on a readeable dat string.\n",
    "        \"\"\"\n",
    "        s:str = ''\n",
    "        for key, value in self.dict().items():\n",
    "            #add the key\n",
    "            s = s + key + \" \" + str(value) + \"\\n\"\n",
    "        return s\n",
    "    \n",
    "    @classmethod\n",
    "    def from_dat(cls: Type['Model'], dat: str)-> 'Model':\n",
    "        \"\"\"\n",
    "        Gets a input.dat file and outputs a dict of parameters.\n",
    "\n",
    "        Args:\n",
    "            filename (string): The full path to the file that will be read.\n",
    "        Returns:\n",
    "            The contents of the file as a dict.\n",
    "        \"\"\"\n",
    "        obj = {}\n",
    "        \n",
    "        for line in dat:\n",
    "            if re.search('[A-Z1-9_]{2,}', line, re.DOTALL):\n",
    "                pair = line.strip('\\r').strip('\\n').split(' ')\n",
    "                pair =list(filter(None, pair)) ## delete empty lists\n",
    "                if len(pair)>2: #remove lists that have more values\n",
    "                    continue\n",
    "                assert len(pair) == 2 # if the list has more or less attribute than 2 it means we have make a mistake spliting stuff\n",
    "                obj[pair[0]] = num(pair[1].strip(' '))\n",
    "                \n",
    "        return cls.parse_obj(obj)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "b8e66d5e",
   "metadata": {},
   "outputs": [],
   "source": [
    "class conservationFeature(BaseModel):\n",
    "    \"\"\"\n",
    "    The Conservation Feature File contains information about each of the conservation\n",
    "    features being considered, such as their name, target representation, and the penalty\n",
    "    if the representation target is not met. It has the default name ‘spec.dat’. Because of\n",
    "    this name it is sometimes referred to as the Species File, although conservation\n",
    "    features will oftenbe surrogates such as habitat type rather than actual species. \n",
    "    \"\"\"\n",
    "   \n",
    "    id: int = Field(..., title='Conservation Feature ID', \n",
    "                    description='A unique numerical identifier for each conservation feature. \\\n",
    "                                Be careful not to duplicate id numbers as Marxan will ignore all but the last one.')\n",
    "    \n",
    "    # All variables you wish to take on Block Definition attributes should\n",
    "    # have their value entered as -1 in the Conservation Feature File\n",
    "    type: Optional[int] = Field(title='Conservation Feature Type', \n",
    "                                description='Used to define groups of conservation features for which a number of \\\n",
    "                                umbrella attributes can be set for all features within the specified group (or “type”). \\\n",
    "                                Each group of features must have a unique numerical identifier. This variable is used \\\n",
    "                                in conjunction with the Block Definition File (see Section 3.3.2) which will contain \\\n",
    "                                the attributes to be assigned to a particular group of conservation features.')\n",
    "    \n",
    "    # If Block Definition File is being used for this feature, then the Target for Feature \n",
    "    # Occurrences should be set to -1 here.\n",
    "    # Target or prop; they are excluding\n",
    "    target: Optional[float] = Field(title=' Feature Representation Target', \n",
    "                          description='The target amount of each conservation feature to be included \\\n",
    "                                        in the solutions. These values represent constraints on potential solutions \\\n",
    "                                        to the reserve selection problem. That is, for a reserve solution to be \\\n",
    "                                        feasible it must include at least this amount of each feature. The target \\\n",
    "                                        value is expressed in the same units used to define the amount of each feature in \\\n",
    "                                        each planning unit, contained in the Planning Unit versus Conservation Feature \\\n",
    "                                        File (see Section 3.2.4). However, units from different conservation features can vary \\\n",
    "                                        (e.g. hectares of habitat for one feature and number of occurrences for another, nests \\\n",
    "                                        for a third and length of stream for a fourth).')\n",
    "    \n",
    "    # If Block Definition File is being used for this feature, then the Target for Feature \n",
    "    # Occurrences should be set to -1 here.\n",
    "    prop: Optional[float] = Field(title='Proportion Target for Feature Representation', \n",
    "                                description='The variable ‘prop’, is short for proportion and can \\\n",
    "                                            be used to set the proportion (i.e. percentage) of a \\\n",
    "                                            conservation feature to be included in the reserve system.',\n",
    "                               ge =0, le =1)\n",
    "    \n",
    "    # If Block Definition File is being used for this feature, then the Target for Feature \n",
    "    # Occurrences should be set to -1 here.\n",
    "    \n",
    "    spf: float = Field(..., title='Conservation Feature Penalty Factor', \n",
    "                       description='The letters ‘spf’ stands for Species Penalty Factor. This \\\n",
    "                                    variable is more correctly referred to as the Conservation Feature Penalty \\\n",
    "                                    Factor.')\n",
    "    target2: Optional[float] = Field(title='Minimum Clump Size', \n",
    "                                     description='This variable specifies a minimum clump size for the\\\n",
    "                                                representation of conservation features in the reserve system. If the amount\\\n",
    "                                                of a conservation feature found in a clump is less that this value, then it does\\\n",
    "                                                not count towards meeting the conservation target')\n",
    "    \n",
    "    # If Block Definition File is being used for this feature, then the Target for Feature \n",
    "    # Occurrences should be set to -1 here.\n",
    "    targetocc: Optional[float] = Field(title='Target for Feature Occurrences', \n",
    "                                       description='This variable specifies the minimum number of occurrences of a\\\n",
    "                                                    conservation feature required in a reserve system. This value can be used in\\\n",
    "                                                    situations where even though your conservation target may be met in one planning\\\n",
    "                                                    unit, you would like it to be represented in a greater number of planning units,\\\n",
    "                                                    possibly for risk spreading')\n",
    "    \n",
    "    name: Optional[str] = Field(title='Conservation Feature Name', \n",
    "                                description='The alphabetical (no numbers!) name of each conservation feature')\n",
    "    \n",
    "    # If Block Definition File is being used for this feature, then the Target for Feature \n",
    "    # Occurrences should be set to -1 here.\n",
    "    sepnum: Optional[float] = Field(title='Target for Separated Feature Occurrences', \n",
    "                                    description='The number of mutually separated occurrences of a feature \\\n",
    "                                                required in the reserve system')\n",
    "    \n",
    "    # If Block Definition File is being used for this feature, then the Target for Feature \n",
    "    # Occurrences should be set to -1 here.\n",
    "    sepdistance: Optional[float] = Field(title='Minimum Separation Distance', \n",
    "                                      description=' Used in conjunction with ‘sepnum’ (above), this variable specifies\\\n",
    "                                                    the minimum distance at which planning units holding a conservation feature\\\n",
    "                                                    are considered to be separate.')\n",
    "\n",
    "     \n",
    "#     Add validator to check either target or prop are present\n",
    "#     #But doesn't trigger\n",
    "#     @validator('prop')\n",
    "#     def check_prop_or_target(cls, v, values):\n",
    "#         if 'target' not in values and not prop:\n",
    "#             raise ValueError('either field target or prop is required')\n",
    "#         return prop\n",
    "    \n",
    "                 \n",
    "class planningUnits(BaseModel):\n",
    "    \"\"\"\n",
    "    The Planning Unit File contains all the information related to planning units, except\n",
    "    for the distribution of conservation features across planning units (which is held in the\n",
    "    Planning Unit versus Conservation Feature File ). The default name for this file is\n",
    "    ‘pu.dat’.\n",
    "    \"\"\"\n",
    "    id: int = Field(..., title=' Planning Unit ID', \n",
    "                    description='A unique numerical identifier for each planning unit')\n",
    "    cost: Optional[float] = Field(1, title='Planning Unit Cost', \n",
    "                        description='The cost of including each planning unit in the reserve system. ')\n",
    "    status: Optional[int] = Field(0, title='Planning Unit Status', \n",
    "                          description='This variable defines whether a planning unit (PU) is locked in or out of\\\n",
    "                                      the initial and final reserve systems. It can take one of four values:')\n",
    "    \n",
    "    # This variable is only required if a minimum separation between feature occurrences has been specified in the\n",
    "    # ‘sepdistance’ column of the Conservation Feature File \n",
    "    xloc: Optional[float] = Field(title='X Planning Unit Location', \n",
    "                          description='The x-axis coordinate of the planning unit')\n",
    "    \n",
    "    # This variable is only required if a minimum separation between feature occurrences has been specified in the\n",
    "    # ‘sepdistance’ column of the Conservation Feature File \n",
    "    yloc: Optional[float] = Field(title='Y Planning Unit Location', \n",
    "                          description='The y-axis coordinate of the planning unit')\n",
    "    \n",
    "    @validator('status')\n",
    "    def status_is_valid(cls, method: int) -> int:\n",
    "        allowed_set = {\n",
    "                       0: 'The PU is not guaranteed to be in the initial reserve',\n",
    "                       1: 'The PU will be included in the initial reserve',\n",
    "                       2: 'The PU is fixed in the reserve system (“locked in”).\\\n",
    "                           It starts in the initial reserve system and cannot be removed.',\n",
    "                       3: 'The PU is fixed outside the reserve system (“locked out”).\\\n",
    "                       It is not included in the initial reserve system and cannot be added.'\n",
    "                      }\n",
    "        \n",
    "        if method not in allowed_set.keys():\n",
    "            raise ValueError(f\"must be in {allowed_set}, got '{method}'\")\n",
    "        \n",
    "        return method\n",
    "\n",
    "\n",
    "class planningUnitVSConservationFeatureV(BaseModel):\n",
    "    \"\"\"\n",
    "    The Planning Unit versus Conservation Feature File contains information on the\n",
    "    distribution of conservation features across planning units. It has the default file\n",
    "    name, ‘puvpsr2.dat’. There are two different formats this file can take, vertical and\n",
    "    horizontal. Either is acceptable and Marxan will test the header line to determine\n",
    "    which format is being used. This one represent the vertical format\n",
    "    \"\"\"\n",
    "    species: int = Field(..., title='Conservation Feature ID', \n",
    "                         description='The unique id number of each conservation feature. This must \\\n",
    "                                    correspond to the id numbers used in the Conservation Feature File.')\n",
    "    pu: int = Field(..., title='Planning Unit ID', \n",
    "                         description='The id of a planning unit where the conservation feature listed on \\\n",
    "                                    the same row occurs. The planning unit id numbers must correspond \\\n",
    "                                    to the numbers used in the Planning Unit File')\n",
    "    amount: float = Field(..., title='Conservation Feature Amount', \n",
    "                         description='The amount of the conservation feature occurring in the planning unit \\\n",
    "                                      listed on the same row. This amount may be related to the abundance \\\n",
    "                                      of a species or the extent of a certain habitat type.')\n",
    "#                           gt=0) # marxan will ignore o amounts\n",
    "\n",
    "class planningUnitVSConservationFeatureH(BaseModel):\n",
    "    \"\"\"\n",
    "    The Planning Unit versus Conservation Feature File contains information on the\n",
    "    distribution of conservation features across planning units. It has the default file\n",
    "    name, ‘puvpsr2.dat’. There are two different formats this file can take, vertical and\n",
    "    horizontal. Either is acceptable and Marxan will test the header line to determine\n",
    "    which format is being used. . This one represent the horizontal format:\n",
    "    the Planning Unit versus Conservation Feature File is simply a matrix of \n",
    "    planning units versus conservation features.\n",
    "    \"\"\"\n",
    "    pu: int = Field(..., title='Planning Unit ID',\n",
    "                    description='Amount of output displayed on the program screen')\n",
    "    species: List[int] = Field(..., title='Conservation Feature IDs', \n",
    "                               description='The unique id number of each conservation feature.')\n",
    "    amount: List[float] = Field(..., title='Conservation Feature Amount', \n",
    "                                 description='The amount of the conservation feature occurring in the planning unit \\\n",
    "                                             listed on the same row',\n",
    "                                 gt=0)\n",
    "\n",
    "\n",
    "### Optional data file structures\n",
    "\n",
    "class boundaryLength(BaseModel):\n",
    "    \"\"\"\n",
    "    The Boundary Length File contains information about the length or ‘effective length’\n",
    "    of shared boundaries between planning units. This file is necessary if you wish to use\n",
    "    the Boundary Length Modifier to improve the compactness of reserve solutions (bound.dat).\n",
    "    \n",
    "    !Any missing values within the file will prevent Marxan from running, for instance \n",
    "    !if ‘id1’ and ‘id2’ are set but no value for ‘boundary’ is entered.\n",
    "    \"\"\"\n",
    "    # important not to duplicate boundaries\n",
    "    id1: int = Field(..., title='Planning Unit ID', \n",
    "                     description='‘id1’ and ‘id2’ contain the id number of the two \\\n",
    "                     planning units that share a boundary.')\n",
    "    id2: int = Field(..., title=' Planning Unit ID', \n",
    "                     description='id1’ and ‘id2’ contain the id number of the two \\\n",
    "                     planning units that share a boundary.')\n",
    "    boundary: float = Field(..., title='Boundary Length', \n",
    "                           description='Boundary Length or boundary cost is relative \\\n",
    "                           measure of how important it is to include one planning unit \\\n",
    "                           in the reserve system, given the inclusion of the other.')\n",
    "        \n",
    "class blockDefinition(BaseModel):\n",
    "    \"\"\"\n",
    "    The Block Definition File is very similar to the Conservation Feature File (see\n",
    "    Section 3.2.2) and is used to set default variable values for groups of conservation\n",
    "    features. It is always used in conjunction with the Conservation Feature File.\n",
    "    \"\"\"\n",
    "    type: int = Field(..., title='Conservation Feature Type', \n",
    "                      description='A unique numerical identifier for groups of conservation features. \\\n",
    "                                   Each ‘type’ must correspond exactly with the types identified \\\n",
    "                                   in the Conservation Feature File ')\n",
    "    \n",
    "    # If this is set Planning Unit versus Conservation Feature File ‘target’, should be set to ‘-1’.\n",
    "    prop: Optional[float] = Field(default=..., title='Proportion Target for Feature Representation', \n",
    "                                description='The variable ‘prop’, is short for proportion and can \\\n",
    "                                            be used to set the proportion (i.e. percentage) of a \\\n",
    "                                            conservation feature to be included in the reserve system.',\n",
    "                               ge =0, le =1)\n",
    "        \n",
    "    target: float = Field(..., title=' Feature Representation Target', \n",
    "                          description='The target amount of each conservation feature to be included \\\n",
    "                                        in the solutions. These values represent constraints on potential solutions \\\n",
    "                                        to the reserve selection problem. That is, for a reserve solution to be \\\n",
    "                                        feasible it must include at least this amount of each feature. The target \\\n",
    "                                        value is expressed in the same units used to define the amount of each feature in \\\n",
    "                                        each planning unit, contained in the Planning Unit versus Conservation Feature \\\n",
    "                                        File (see Section 3.2.4). However, units from different conservation features can vary \\\n",
    "                                        (e.g. hectares of habitat for one feature and number of occurrences for another, nests \\\n",
    "                                        for a third and length of stream for a fourth).')   \n",
    "    spf: float = Field(-1, title='Conservation Feature Penalty Factor', \n",
    "                       description='The letters ‘spf’ stands for Species Penalty Factor. This \\\n",
    "                                    variable is more correctly referred to as the Conservation Feature Penalty \\\n",
    "                                    Factor.')\n",
    "    target2: Optional[float] = Field(-1, title='Minimum Clump Size', \n",
    "                                     description='This variable specifies a minimum clump size for the \\\n",
    "                                                representation of conservation features in the reserve system. If the amount\\\n",
    "                                                of a conservation feature found in a clump is less that this value, then it does\\\n",
    "                                                not count towards meeting the conservation target')\n",
    "    \n",
    "    targetocc: Optional[float] = Field(-1, title='Target for Feature Occurrences', \n",
    "                                       description='This variable specifies the minimum number of occurrences of a\\\n",
    "                                                    conservation feature required in a reserve system. This value can be used in\\\n",
    "                                                    situations where even though your conservation target may be met in one planning\\\n",
    "                                                    unit, you would like it to be represented in a greater number of planning units,\\\n",
    "                                                    possibly for risk spreading')\n",
    "\n",
    "    sepnum: Optional[float] = Field(-1, title='Target for Separated Feature Occurrences', \n",
    "                                  description='The number of mutually separated occurrences of a feature \\\n",
    "                                                required in the reserve system')\n",
    "    \n",
    "    sepdistance: Optional[float] = Field(-1, title='Minimum Separation Distance', \n",
    "                                      description=' Used in conjunction with ‘sepnum’ (above), this variable specifies\\\n",
    "                                                    the minimum distance at which planning units holding a conservation feature\\\n",
    "                                                    are considered to be separate.')    "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b9ceec66",
   "metadata": {},
   "source": [
    "### Output dat model types"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "4f625ad4",
   "metadata": {},
   "outputs": [],
   "source": [
    "class OutputRun(BaseModel): \n",
    "    \"\"\"\n",
    "    Class of files _r00001.csv and _best.csv\n",
    "    A file is produced for each repeat run containing a list of all the planning units selected in the solution for that run\n",
    "    \"\"\"\n",
    "# General Parameters\n",
    "    PUID: int = Field(title='Planning Unit id', \n",
    "                         description='List of planning units')\n",
    "    SOLUTION:  int = Field(title='Solution', \n",
    "                           description='Planning unit included in this solution if 1, not included if 0',\n",
    "                           ge =0, le =1)\n",
    "        \n",
    "class OutputMV(BaseModel):\n",
    "    \"\"\"\n",
    "    Class of files _mv00001.csv or _mvbest.csv\n",
    "    This file contains information about the representation of conservation features in the solution for each run.\\\n",
    "    The file contains a total of nine columns which basically report on how the solution performed relative to the targets.\\\n",
    "    Some of these are simply a summary of the information provided in the Conservation Feature File\n",
    "    \"\"\"\n",
    "# General Parameters\n",
    "    Conservation_Feature: int = Field(title = 'Conservation feature id',\n",
    "                                      description ='The unique ID number of the conservation feature')\n",
    "    Feature_Name: str = Field(title = 'Feature Name',\n",
    "                               description = 'The optional alphabetic name of the conservation feature.\\\n",
    "                               If no name has been specified then nothing will appear in this column.')\n",
    "    Target: float = Field(title= 'target',\n",
    "                           description = 'The target level of representation (if any) for that conservation feature')\n",
    "    Amount_Held: float = Field(title = 'Amount held',\n",
    "                                description = 'The amount of that conservation feature captured in the reserve system')\n",
    "    Occurrence_Target: float = Field(title = 'Occurrence target',\n",
    "                                     description ='The target number of occurrences in the reserve system for that conservation feature')\n",
    "    Occurrences_Held: float = Field(title= 'Ocurrence s held',\n",
    "                                    description= 'The number of occurrences of the conservation feature captured in the solution.\\\n",
    "                                    Again, only occurrences in valid clumps are included')\n",
    "    Separation_Target: float = Field (title= 'Separation target',\n",
    "                                     description = 'The number of mutually and adequately separated occurrences of that\\\n",
    "                                     conservation feature required in the reserve system')\n",
    "    Separation_Achieved: float = Field(title = 'Separation Achieved',\n",
    "                                      description= 'The number reported here will be the lowest of either: the number of \\\n",
    "                                      separate occurrences that are actually achieved in the reserve system ; or the target \\\n",
    "                                      number of separate occurrences. The separation count (see Appendix B-1.3.1) never exceeds \\\n",
    "                                      the separation target for that feature. This is a convention which speeds up the execution \\\n",
    "                                      of the software but it means that no information is given about how far this target is exceeded.' )\n",
    "    Target_Met: str = Field(title= 'Target met',\n",
    "                           description = 'An alphabetic variable that returns ‘yes’ if all the targets set for that feature are met,\\\n",
    "                           otherwise it returns ‘no’')\n",
    "    MPM: float = Field (title= 'Minimum Proportion Met',\n",
    "                       description= 'Propotion of target met, min in 0 max i 1',\n",
    "                       ge =0, le =1)\n",
    "        \n",
    "class OutputSum(BaseModel):\n",
    "    \"\"\"\n",
    "    Class of files _sum.csv\n",
    "    This file contains the summary information for each repeat run.\\\n",
    "    It contains nine columns, which basically report on how the solution performed relative to the targets\n",
    "    \"\"\"\n",
    "    Run_Number: int = Field(title='Run number',\n",
    "                         description='Which of the repeat runs (or solutions) the output pertains to')\n",
    "    Score: float = Field(title= 'Score',\n",
    "                       description ='This is the overall objective function value for the solution from that run.\\\n",
    "                       This includes not only the cost of the planning units and the boundary length but also the penalties\\\n",
    "                       for failing to adequately represent all conservation features or exceeding the cost threshold.\\\n",
    "                       It is useful to know this value because it is how Marxan chooses the ‘best’ solution out of your repeat runs.')\n",
    "    Cost: float = Field(title='Cost',\n",
    "                     description = 'This is the total cost of the reserve system as determined solely by the costs given to each planning unit.')\n",
    "    Planning_Units: int = Field(title= 'Planning Units',\n",
    "                               description = 'The number of planning units contained in the solution for that run')\n",
    "    Connectivity: float = Field (title ='Connectivity',\n",
    "                                description= 'The total boundary length of the reserve system.\\\n",
    "                                If boundary length is not being considered in the analyses (i.e. no Boundary Length File is provided),\\\n",
    "                                then this value will read ‘0.0’.')\n",
    "    Connectivity_Total: float = Field (title ='Connectivity Total',\n",
    "                                description= 'Total boundary of planning units in study area.')\n",
    "    Connectivity_In:float = Field (title ='Connectivity In',\n",
    "                                description= 'Sum of shared boundary between selected planning units.')\n",
    "    Connectivity_Edge:float = Field (title ='Connectivity Edge',\n",
    "                                description= 'Same as Connectivity boundary length')\n",
    "    Connectivity_Out:float = Field (title ='Connectivity Out',\n",
    "                                description= 'Sum of the outer boundaries of unselected planning units.')\n",
    "    Connectivity_In_Fraction: float = Field (title ='Connectivity In Fraction',\n",
    "                                description= 'Connectivity_In/Connectivity_Total - the larger this fraction,\\\n",
    "                                the more spatially compact the solution.')\n",
    "    Penalty: float = Field(title = 'Penalty',\n",
    "                           description = 'The penalty that was added to the objective function because the reserve system\\\n",
    "                           failed to meet the representation targets for all features. If all features are adequately represented\\\n",
    "                           then the penalty value will be either 0.0 or “-0.0”. (Because of round-off error it is not likely to be\\\n",
    "                           exactly equal to 0, but with only one decimal place presented the round-off error will probably be hidden).\\\n",
    "                           The penalty is useful to know because it can give you an idea of the cost required to meet the remaining targets,\\\n",
    "                           this is something that is not captured simply by looking at the shortfall. It is also another way to rank\\\n",
    "                           the success of runs, looking only at those solutions that have a low penalty.')\n",
    "    Shortfall: float = Field(title ='Shortfall',\n",
    "                            description = 'The amount by which the targets for conservation features have not been met\\\n",
    "                            in the solution for that run. The shortfall reported here is the total shortfall summed across\\\n",
    "                            all conservation features. The shortfall is a good indication of whether missing conservation\\\n",
    "                            features are very close or very far from their targets. If there are a number of conservation\\\n",
    "                            features which have missed their targets but the combined shortfall is very sma ll then a planner\\\n",
    "                            might not be too concerned.')\n",
    "    Missing_Values: int = Field (title ='Missing Values',\n",
    "                                description= 'The number of features that did not achieve their targets in the final solution for that run.\\\n",
    "                                This is screened according to the ‘misslevel’, which has been set in the Input Parameter File.\\\n",
    "                                If the miss level is set to 1 then every conservation feature which falls below its target level\\\n",
    "                                is counted as missing. If the miss level is set lower than 1 (e.g. 0.98), Marxan may not report a\\\n",
    "                                feature as missing even if the reserve system contains slightly less than the target amount.')\n",
    "    MPM: float = Field(title ='Minimum Proportion Met',\n",
    "                    description= 'The Minimum Proportion Met for the worst performing feature.\\\n",
    "                    That is, this value corresponds to the lowest MPM value in the missing value file.')\n",
    "        \n",
    "\n",
    "class OutputSsoln(BaseModel):\n",
    "    \"\"\"\n",
    "    Class of files _ssoln.csv\n",
    "    Summed solution provides the selection frequency of each planning unit across all runs.\\\n",
    "    Each line has the ID number of a planning unit and the number of times\\\n",
    "    that planning unit was selected in the final solution across all repeat runs \n",
    "    \"\"\"\n",
    "    planning_unit: int = Field(title='Planning Unit',\n",
    "                              description= 'ID number of a planning unit')\n",
    "    number: int = Field(title= 'Number',\n",
    "                       description= 'Number of times a planning unit was selected in the final solution across all repeat runs')\n",
    "        \n",
    "        \n",
    "# OutputSolMat\n",
    "### Depends on the number of Planning Units:\n",
    "### Create a class dynamically depending on the number of planning units of the file\n",
    "### https://stackoverflow.com/questions/62267544/pydantic-generate-model-from-dict\n",
    "#OutputSolMat = createDynamicModel(filename= filename, name= 'OutputSolMat', dict_def= d)\n",
    "\n",
    "class OutputSolutionsMatrix(BaseModel):\n",
    "    SolutionsMatrix: str = Field(..., title='Solution number',\n",
    "                    description='Solution number')\n",
    "    PU: Dict[str, float] = Field(..., title='Planning unit selection', \n",
    "                               description='Planning units selected in each solution. The dictionary parameters represent:\\\n",
    "                               key = Planning unit number (P1, P2, P3...), value= selection in this solution (0= False, 1 = True)')\n",
    "    \n",
    "#     @validator('SolutionsMatrix')\n",
    "#     # Check that the number of output solutions is the same as in the input.dat file\n",
    "#     def SolutionsMatrix_is_valid(cls, method:str) -> str:\n",
    "#         InputFile = DatFile(f'{MARXAN_FOLDER}/input.dat')\n",
    "#         InputFile.read()\n",
    "#         userInputFile = inputDatFile.from_dat(InputFile.data)\n",
    "#         solNum = userInputFile.NUMREPS\n",
    "#         if len('SolutionsMatrix') != solNum:\n",
    "#             raise ValueError(f\"Solutions in input file is {solNum} but got {len(SolutionsMatrix)}\")\n",
    "#         return method\n",
    "        \n",
    "    \n",
    "#     @validator('PU')\n",
    "#     # Check that the number of output planning units is the same as in the pu.dat file\n",
    "#     def PU_is_valid(cls, method:dict) -> dict:\n",
    "#         InputFile = DatFile(f'{MARXAN_FOLDER}/input.dat')\n",
    "#         InputFile.read()\n",
    "#         userInputFile = inputDatFile.from_dat(InputFile.data)\n",
    "#         filename = f'{MARXAN_FOLDER}/{userInputFile.INPUTDIR}/{userInputFile.PUNAME}'\n",
    "#         userPlanningUnits = CreateListModelFromFile(f'{MARXAN_FOLDER}/{userInputFile.INPUTDIR}/{userInputFile.PUNAME}', planningUnits)\n",
    "#         puNum =len(userPlanningUnits)\n",
    "#         if len(PU) != puNum:\n",
    "#             raise ValueError(f\"PU in input file is {puNum} but got {len(PU)}\")\n",
    "#         return method"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1e2e5046",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
